/*
 * Copyright (C) 2007-2010 Júlio Vilmar Gesser.
 * Copyright (C) 2011, 2013-2024 The JavaParser Team.
 *
 * This file is part of JavaParser.
 *
 * JavaParser can be used either under the terms of
 * a) the GNU Lesser General Public License as published by
 *     the Free Software Foundation, either version 3 of the License, or
 *     (at your option) any later version.
 * b) the terms of the Apache License
 *
 * You should have received a copy of both licenses in LICENCE.LGPL and
 * LICENCE.APACHE. Please refer to those files for details.
 *
 * JavaParser is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 */

package com.github.javaparser.printer.lexicalpreservation.transformations.ast.body;

import static com.github.javaparser.StaticJavaParser.parseExpression;
import static com.github.javaparser.StaticJavaParser.parseStatement;
import static com.github.javaparser.ast.Modifier.Keyword.PROTECTED;
import static com.github.javaparser.ast.Modifier.Keyword.PUBLIC;
import static com.github.javaparser.ast.Modifier.createModifierList;
import static com.github.javaparser.utils.TestUtils.assertEqualsStringIgnoringEol;

import com.github.javaparser.ast.Modifier;
import com.github.javaparser.ast.NodeList;
import com.github.javaparser.ast.body.MethodDeclaration;
import com.github.javaparser.ast.body.Parameter;
import com.github.javaparser.ast.comments.MarkdownComment;
import com.github.javaparser.ast.expr.Expression;
import com.github.javaparser.ast.expr.MarkerAnnotationExpr;
import com.github.javaparser.ast.expr.SimpleName;
import com.github.javaparser.ast.expr.StringLiteralExpr;
import com.github.javaparser.ast.stmt.Statement;
import com.github.javaparser.ast.type.ArrayType;
import com.github.javaparser.ast.type.PrimitiveType;
import com.github.javaparser.javadoc.Javadoc;
import com.github.javaparser.javadoc.description.JavadocDescription;
import com.github.javaparser.printer.lexicalpreservation.AbstractLexicalPreservingTest;
import com.github.javaparser.printer.lexicalpreservation.LexicalPreservingPrinter;
import com.github.javaparser.utils.LineSeparator;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;

/**
 * Transforming MethodDeclaration and verifying the LexicalPreservation works as expected.
 */
class MethodDeclarationTransformationsTest extends AbstractLexicalPreservingTest {

    protected MethodDeclaration consider(String code) {
        considerCode("class A { " + code + " }");
        return cu.getType(0).getMembers().get(0).asMethodDeclaration();
    }

    // Name

    @Test
    void settingName() {
        MethodDeclaration it = consider("void A(){}");
        it.setName("B");
        assertTransformedToString("void B(){}", it);
    }

    // JavaDoc

    @Test
    void removingDuplicateJavaDocComment() {
        // Arrange
        considerCode("public class MyClass {" + LineSeparator.SYSTEM + LineSeparator.SYSTEM
                + "  /**"
                + LineSeparator.SYSTEM + "   * Comment A"
                + LineSeparator.SYSTEM + "   */"
                + LineSeparator.SYSTEM + "  public void oneMethod() {"
                + LineSeparator.SYSTEM + "  }"
                + LineSeparator.SYSTEM + LineSeparator.SYSTEM
                + "  /**"
                + LineSeparator.SYSTEM + "   * Comment A"
                + LineSeparator.SYSTEM + "   */"
                + LineSeparator.SYSTEM + "  public void anotherMethod() {"
                + LineSeparator.SYSTEM + "  }"
                + LineSeparator.SYSTEM + "}"
                + LineSeparator.SYSTEM);

        MethodDeclaration methodDeclaration =
                cu.findAll(MethodDeclaration.class).get(1);

        // Act
        methodDeclaration.removeComment();

        // Assert
        String result = LexicalPreservingPrinter.print(cu.findCompilationUnit().get());
        assertEqualsStringIgnoringEol(
                "public class MyClass {\n" + "\n"
                        + "  /**\n"
                        + "   * Comment A\n"
                        + "   */\n"
                        + "  public void oneMethod() {\n"
                        + "  }\n"
                        + "\n"
                        + "  public void anotherMethod() {\n"
                        + "  }\n"
                        + "}\n",
                result);
    }

    @Disabled
    @Test
    void replacingDuplicateJavaDocComment() {
        // Arrange
        considerCode("public class MyClass {" + LineSeparator.SYSTEM + LineSeparator.SYSTEM
                + "  /**"
                + LineSeparator.SYSTEM + "   * Comment A"
                + LineSeparator.SYSTEM + "   */"
                + LineSeparator.SYSTEM + "  public void oneMethod() {"
                + LineSeparator.SYSTEM + "  }"
                + LineSeparator.SYSTEM + LineSeparator.SYSTEM
                + "  /**"
                + LineSeparator.SYSTEM + "   * Comment A"
                + LineSeparator.SYSTEM + "   */"
                + LineSeparator.SYSTEM + "  public void anotherMethod() {"
                + LineSeparator.SYSTEM + "  }"
                + LineSeparator.SYSTEM + "}"
                + LineSeparator.SYSTEM);

        MethodDeclaration methodDeclaration =
                cu.findAll(MethodDeclaration.class).get(1);

        // Act
        Javadoc javadoc = new Javadoc(JavadocDescription.parseText("Change Javadoc"));
        methodDeclaration.setJavadocComment("", javadoc);

        // Assert
        String result = LexicalPreservingPrinter.print(cu.findCompilationUnit().get());
        assertEqualsStringIgnoringEol(
                "public class MyClass {\n" + "\n"
                        + "  /**\n"
                        + "   * Comment A\n"
                        + "   */\n"
                        + "  public void oneMethod() {\n"
                        + "  }\n"
                        + "\n"
                        + "  /**\n"
                        + "   * Change Javadoc\n"
                        + "   */\n"
                        + "  public void anotherMethod() {\n"
                        + "  }\n"
                        + "}\n",
                result);
    }

    @Test
    void replacingDuplicateMarkdownComment() {
        // Arrange
        considerCode("public class MyClass {" + LineSeparator.SYSTEM + LineSeparator.SYSTEM
                + "  ///"
                + LineSeparator.SYSTEM + "  /// Comment A"
                + LineSeparator.SYSTEM + "  ///"
                + LineSeparator.SYSTEM + "  public void oneMethod() {"
                + LineSeparator.SYSTEM + "  }"
                + LineSeparator.SYSTEM + LineSeparator.SYSTEM
                + "  ///"
                + LineSeparator.SYSTEM + "  /// Comment A"
                + LineSeparator.SYSTEM + "  ///"
                + LineSeparator.SYSTEM + "  public void anotherMethod() {"
                + LineSeparator.SYSTEM + "  }"
                + LineSeparator.SYSTEM + "}"
                + LineSeparator.SYSTEM);

        MethodDeclaration methodDeclaration =
                cu.findAll(MethodDeclaration.class).get(1);

        // Act
        methodDeclaration.setComment(new MarkdownComment("///\n  /// Change Markdown\n  ///"));

        // Assert
        String result = LexicalPreservingPrinter.print(cu.findCompilationUnit().get());
        assertEqualsStringIgnoringEol(
                "public class MyClass {\n" + "\n"
                        + "  ///\n"
                        + "  /// Comment A\n"
                        + "  ///\n"
                        + "  public void oneMethod() {\n"
                        + "  }\n"
                        + "\n"
                        + "  ///\n"
                        + "  /// Change Markdown\n"
                        + "  ///\n"
                        + "  public void anotherMethod() {\n"
                        + "  }\n"
                        + "}\n",
                result);
    }

    @Test
    void removingMarkdownComment() {
        // Arrange
        considerCode("public class MyClass {" + LineSeparator.SYSTEM + LineSeparator.SYSTEM
                + "  ///"
                + LineSeparator.SYSTEM + "  /// Comment A"
                + LineSeparator.SYSTEM + "  ///"
                + LineSeparator.SYSTEM + "  public void oneMethod() {"
                + LineSeparator.SYSTEM + "  }"
                + LineSeparator.SYSTEM + "}"
                + LineSeparator.SYSTEM);

        MethodDeclaration methodDeclaration =
                cu.findAll(MethodDeclaration.class).get(0);

        // Act
        methodDeclaration.removeComment();
        // Assert
        String result = LexicalPreservingPrinter.print(cu.findCompilationUnit().get());
        assertEqualsStringIgnoringEol(
                "public class MyClass {\n" + "\n" + "  public void oneMethod() {\n" + "  }\n" + "}\n", result);
    }

    @Test
    void addingMarkdownComment() {
        // Arrange
        considerCode("public class MyClass {"
                + LineSeparator.SYSTEM + "  public void oneMethod() {"
                + LineSeparator.SYSTEM + "  }"
                + LineSeparator.SYSTEM + "}"
                + LineSeparator.SYSTEM);

        MethodDeclaration methodDeclaration =
                cu.findAll(MethodDeclaration.class).get(0);

        // Act
        MarkdownComment markdownComment = new MarkdownComment("///\n  /// Comment A\n  ///");
        methodDeclaration.setComment(markdownComment);
        // Assert
        String result = LexicalPreservingPrinter.print(cu.findCompilationUnit().get());
        assertEqualsStringIgnoringEol(
                "public class MyClass {\n"
                        + "  ///\n"
                        + "  /// Comment A\n"
                        + "  ///\n"
                        + "  public void oneMethod() {\n"
                        + "  }\n"
                        + "}\n",
                result);
    }

    // Comments

    @Test
    void removingDuplicateComment() {
        // Arrange
        considerCode("public class MyClass {" + LineSeparator.SYSTEM + LineSeparator.SYSTEM
                + "  /*"
                + LineSeparator.SYSTEM + "   * Comment A"
                + LineSeparator.SYSTEM + "   */"
                + LineSeparator.SYSTEM + "  public void oneMethod() {"
                + LineSeparator.SYSTEM + "  }"
                + LineSeparator.SYSTEM + LineSeparator.SYSTEM
                + "  /*"
                + LineSeparator.SYSTEM + "   * Comment A"
                + LineSeparator.SYSTEM + "   */"
                + LineSeparator.SYSTEM + "  public void anotherMethod() {"
                + LineSeparator.SYSTEM + "  }"
                + LineSeparator.SYSTEM + "}"
                + LineSeparator.SYSTEM);

        MethodDeclaration methodDeclaration =
                cu.findAll(MethodDeclaration.class).get(1);

        // Act
        methodDeclaration.removeComment();

        // Assert
        String result = LexicalPreservingPrinter.print(cu.findCompilationUnit().get());
        assertEqualsStringIgnoringEol(
                "public class MyClass {\n" + "\n"
                        + "  /*\n"
                        + "   * Comment A\n"
                        + "   */\n"
                        + "  public void oneMethod() {\n"
                        + "  }\n"
                        + "\n"
                        + "  public void anotherMethod() {\n"
                        + "  }\n"
                        + "}\n",
                result);
    }

    // Modifiers

    @Test
    void addingModifiers() {
        MethodDeclaration it = consider("void A(){}");
        it.setModifiers(createModifierList(PUBLIC));
        assertTransformedToString("public void A(){}", it);
    }

    @Test
    void removingModifiers() {
        MethodDeclaration it = consider("public void A(){}");
        it.setModifiers(new NodeList<>());
        assertTransformedToString("void A(){}", it);
    }

    @Test
    void removingModifiersWithExistingAnnotationsShort() {
        MethodDeclaration it = consider("@Override public void A(){}");
        it.setModifiers(new NodeList<>());
        assertTransformedToString("@Override void A(){}", it);
    }

    @Test
    void removingPublicModifierFromPublicStaticMethod() {
        MethodDeclaration it = consider("public static void a(){}");
        it.removeModifier(Modifier.Keyword.PUBLIC);
        assertTransformedToString("static void a(){}", it);
    }

    @Test
    void removingModifiersWithExistingAnnotations() {
        considerCode("class X {" + LineSeparator.SYSTEM + "  @Test"
                + LineSeparator.SYSTEM + "  public void testCase() {"
                + LineSeparator.SYSTEM + "  }"
                + LineSeparator.SYSTEM + "}"
                + LineSeparator.SYSTEM);

        cu.getType(0).getMethods().get(0).setModifiers(new NodeList<>());

        String result = LexicalPreservingPrinter.print(cu);
        assertEqualsStringIgnoringEol("class X {\n" + "  @Test\n" + "  void testCase() {\n" + "  }\n" + "}\n", result);
    }

    @Test
    void removingModifiersWithExistingAnnotations_withVariableNumberOfSeparator() {
        considerCode("class X {" + LineSeparator.SYSTEM + "  @Test"
                + LineSeparator.SYSTEM + "  public      void testCase() {"
                + LineSeparator.SYSTEM + "  }"
                + LineSeparator.SYSTEM + "}"
                + LineSeparator.SYSTEM);

        cu.getType(0).getMethods().get(0).setModifiers(new NodeList<>());

        String result = LexicalPreservingPrinter.print(cu.findCompilationUnit().get());
        assertEqualsStringIgnoringEol("class X {\n" + "  @Test\n" + "  void testCase() {\n" + "  }\n" + "}\n", result);
    }

    @Test
    void replacingModifiers() {
        MethodDeclaration it = consider("public void A(){}");
        it.setModifiers(createModifierList(PROTECTED));
        assertTransformedToString("protected void A(){}", it);
    }

    @Test
    void replacingModifiersWithExistingAnnotationsShort() {
        MethodDeclaration it = consider("@Override public void A(){}");
        it.setModifiers(createModifierList(PROTECTED));
        assertTransformedToString("@Override protected void A(){}", it);
    }

    @Test
    void replacingModifiersWithExistingAnnotations() {
        considerCode("class X {" + LineSeparator.SYSTEM + "  @Test"
                + LineSeparator.SYSTEM + "  public void testCase() {"
                + LineSeparator.SYSTEM + "  }"
                + LineSeparator.SYSTEM + "}"
                + LineSeparator.SYSTEM);

        cu.getType(0).getMethods().get(0).setModifiers(createModifierList(PROTECTED));

        String result = LexicalPreservingPrinter.print(cu.findCompilationUnit().get());
        assertEqualsStringIgnoringEol(
                "class X {\n" + "  @Test\n" + "  protected void testCase() {\n" + "  }\n" + "}\n", result);
    }

    // Parameters

    @Test
    void addingParameters() {
        MethodDeclaration it = consider("void foo(){}");
        it.addParameter(PrimitiveType.doubleType(), "d");
        assertTransformedToString("void foo(double d){}", it);
    }

    @Test
    void removingOnlyParameter() {
        MethodDeclaration it = consider("public void foo(double d){}");
        it.getParameters().remove(0);
        assertTransformedToString("public void foo(){}", it);
    }

    @Test
    void removingFirstParameterOfMany() {
        MethodDeclaration it = consider("public void foo(double d, float f){}");
        it.getParameters().remove(0);
        assertTransformedToString("public void foo(float f){}", it);
    }

    @Test
    void removingLastParameterOfMany() {
        MethodDeclaration it = consider("public void foo(double d, float f){}");
        it.getParameters().remove(1);
        assertTransformedToString("public void foo(double d){}", it);
    }

    @Test
    void replacingOnlyParameter() {
        MethodDeclaration it = consider("public void foo(float f){}");
        it.getParameters().set(0, new Parameter(new ArrayType(PrimitiveType.intType()), new SimpleName("foo")));
        assertTransformedToString("public void foo(int[] foo){}", it);
    }

    // ThrownExceptions

    // Body

    // Annotations
    @Test
    void addingToExistingAnnotations() {
        considerCode("class X {" + LineSeparator.SYSTEM + "  @Test"
                + LineSeparator.SYSTEM + "  public void testCase() {"
                + LineSeparator.SYSTEM + "  }"
                + LineSeparator.SYSTEM + "}"
                + LineSeparator.SYSTEM);

        cu.getType(0)
                .getMethods()
                .get(0)
                .addSingleMemberAnnotation("org.junit.Ignore", new StringLiteralExpr("flaky test"));

        String result = LexicalPreservingPrinter.print(cu.findCompilationUnit().get());
        assertEqualsStringIgnoringEol(
                "class X {\n" + "  @Test\n"
                        + "  @org.junit.Ignore(\"flaky test\")\n"
                        + "  public void testCase() {\n"
                        + "  }\n"
                        + "}\n",
                result);
    }

    @Test
    void addingAnnotationsNoModifiers() {
        considerCode("class X {" + LineSeparator.SYSTEM + "  void testCase() {"
                + LineSeparator.SYSTEM + "  }"
                + LineSeparator.SYSTEM + "}"
                + LineSeparator.SYSTEM);

        cu.getType(0).getMethods().get(0).addMarkerAnnotation("Test");
        cu.getType(0).getMethods().get(0).addMarkerAnnotation("Override");

        String result = LexicalPreservingPrinter.print(cu.findCompilationUnit().get());
        assertEqualsStringIgnoringEol(
                "class X {\n" + "  @Test\n" + "  @Override\n" + "  void testCase() {\n" + "  }\n" + "}\n", result);
    }

    @Test
    void replacingAnnotations() {
        considerCode("class X {" + LineSeparator.SYSTEM + "  @Override"
                + LineSeparator.SYSTEM + "  public void testCase() {"
                + LineSeparator.SYSTEM + "  }"
                + LineSeparator.SYSTEM + "}"
                + LineSeparator.SYSTEM);

        cu.getType(0).getMethods().get(0).setAnnotations(new NodeList<>(new MarkerAnnotationExpr("Test")));

        String result = LexicalPreservingPrinter.print(cu.findCompilationUnit().get());
        assertEqualsStringIgnoringEol(
                "class X {\n" + "  @Test\n" + "  public void testCase() {\n" + "  }\n" + "}\n", result);
    }

    @Test
    void addingAnnotationsShort() {
        MethodDeclaration it = consider("void testMethod(){}");
        it.addMarkerAnnotation("Override");
        assertTransformedToString("@Override" + LineSeparator.SYSTEM + "void testMethod(){}", it);
    }

    // This test case was disabled because we cannot resolve this case for now
    // because indentation before the removed annotation is not part
    // of difference elements (see removingAnnotationsWithSpaces too)
    @Disabled
    @Test
    void removingAnnotations() {
        considerCode("class X {" + LineSeparator.SYSTEM + "  @Override"
                + LineSeparator.SYSTEM + "  public void testCase() {"
                + LineSeparator.SYSTEM + "  }"
                + LineSeparator.SYSTEM + "}"
                + LineSeparator.SYSTEM);

        cu.getType(0).getMethods().get(0).getAnnotationByName("Override").get().remove();

        String result = LexicalPreservingPrinter.print(cu);
        assertEqualsStringIgnoringEol("class X {\n" + "  public void testCase() {\n" + "  }\n" + "}\n", result);
    }

    @Disabled
    @Test
    void removingAnnotationsWithSpaces() {
        considerCode("class X {" + LineSeparator.SYSTEM + "  @Override "
                + LineSeparator.SYSTEM + "  public void testCase() {"
                + LineSeparator.SYSTEM + "  }"
                + LineSeparator.SYSTEM + "}"
                + LineSeparator.SYSTEM);

        cu.getType(0).getMethods().get(0).getAnnotationByName("Override").get().remove();

        String result = LexicalPreservingPrinter.print(cu);
        assertEqualsStringIgnoringEol("class X {\n" + "  public void testCase() {\n" + "  }\n" + "}\n", result);
    }

    @Test
    public void addingModifiersWithExistingAnnotationsShort() {
        MethodDeclaration it = consider("@Override void A(){}");
        it.setModifiers(NodeList.nodeList(Modifier.publicModifier(), Modifier.finalModifier()));
        assertTransformedToString("@Override public final void A(){}", it);
    }

    @Test
    public void addingModifiersWithExistingAnnotations() {
        considerCode("class X {" + LineSeparator.SYSTEM + "  @Test"
                + LineSeparator.SYSTEM + "  void testCase() {"
                + LineSeparator.SYSTEM + "  }"
                + LineSeparator.SYSTEM + "}"
                + LineSeparator.SYSTEM);

        cu.getType(0)
                .getMethods()
                .get(0)
                .addModifier(
                        Modifier.finalModifier().getKeyword(),
                        Modifier.publicModifier().getKeyword());

        String result = LexicalPreservingPrinter.print(cu.findCompilationUnit().get());
        assertEqualsStringIgnoringEol(
                "class X {\n" + "  @Test\n" + "  final public void testCase() {\n" + "  }\n" + "}\n", result);
    }

    @Test
    public void parseAndPrintAnonymousClassExpression() {
        Expression expression = parseExpression("new Object() {" + LineSeparator.SYSTEM + "}");
        String expected = "new Object() {" + LineSeparator.SYSTEM + "}";
        assertTransformedToString(expected, expression);
    }

    @Test
    public void parseAndPrintAnonymousClassStatement() {
        Statement statement = parseStatement("Object anonymous = new Object() {" + LineSeparator.SYSTEM + "};");
        String expected = "Object anonymous = new Object() {" + LineSeparator.SYSTEM + "};";
        assertTransformedToString(expected, statement);
    }

    @Test
    public void replaceBodyShouldNotBreakAnonymousClasses() {
        MethodDeclaration it = consider("public void method() { }");
        it.getBody().ifPresent(body -> {
            Statement statement = parseStatement("Object anonymous = new Object() {" + LineSeparator.SYSTEM + "};");
            NodeList<Statement> statements = new NodeList<>();
            statements.add(statement);
            body.setStatements(statements);
        });

        String expected = "public void method() {" + LineSeparator.SYSTEM + "    Object anonymous = new Object() {"
                + LineSeparator.SYSTEM + "    };"
                + LineSeparator.SYSTEM + "}";
        assertTransformedToString(expected, it);
    }
}
